package frostillicus.xsp.controller; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.faces.component.UIComponent; import javax.faces.component.UIInput; import javax.faces.component.UIOutput; import javax.faces.component.UISelectItem; import javax.faces.component.UISelectItems; import javax.faces.component.UISelectMany; import javax.faces.component.UISelectOne; import javax.faces.context.FacesContext; import javax.faces.el.ValueBinding; import javax.faces.validator.Validator; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import javax.validation.metadata.ConstraintDescriptor; import org.hibernate.validator.constraints.NotEmpty; import org.openntf.xsp.extlib.query.XspQuery; import com.ibm.commons.util.StringUtil; import com.ibm.xsp.application.ApplicationEx; import com.ibm.xsp.component.UISelectOneMenu; import com.ibm.xsp.component.xp.XspColumn; import com.ibm.xsp.component.xp.XspDateTimeHelper; import com.ibm.xsp.component.xp.XspInputText; import com.ibm.xsp.component.xp.XspOutputText; import com.ibm.xsp.component.xp.XspSelectManyListbox; import com.ibm.xsp.component.xp.XspSelectOneMenu; import com.ibm.xsp.component.xp.XspSelectOneRadio; import com.ibm.xsp.component.xp.XspViewColumn; import com.ibm.xsp.component.xp.XspViewColumnHeader; import com.ibm.xsp.convert.DateTimeConverter; import com.ibm.xsp.extlib.component.data.UIFormLayoutRow; import com.ibm.xsp.extlib.component.data.UIFormTable; import com.ibm.xsp.model.DataObject; import frostillicus.xsp.converter.EnumBindingConverter; import frostillicus.xsp.util.FrameworkUtils; /** * @since 1.0 */ public class ComponentMap implements DataObject, Serializable { private static final long serialVersionUID = 1L; public static final String COMPONENT_MAP_SERVICE_NAME = "frostillicus.xsp.controller.ComponentMapAdapterFactory"; @edu.umd.cs.findbugs.annotations.SuppressWarnings( value="SE_BAD_FIELD", justification="This is cleared during serialization") private Map<Object, ComponentPropertyMap> cache_ = new HashMap<Object, ComponentPropertyMap>(); private Set<String> initialized_ = new HashSet<String>(); private final String controllerPropertyName_; public ComponentMap(final String controllerPropertyName) { controllerPropertyName_ = controllerPropertyName; } @Override public Class<ComponentPropertyMap> getType(final Object key) { return ComponentPropertyMap.class; } @Override public ComponentPropertyMap getValue(final Object key) { if(!cache_.containsKey(key)) { cache_.put(key, new ComponentPropertyMap(key)); } return cache_.get(key); } @Override public boolean isReadOnly(final Object key) { return true; } @Override public void setValue(final Object key, final Object value) { } public void initialize() { for(ComponentPropertyMap map : cache_.values()) { map.initialize(); } } private void writeObject(final java.io.ObjectOutputStream stream) throws IOException { cache_.clear(); stream.defaultWriteObject(); } public class ComponentPropertyMap implements DataObject { private Map<String, UIComponent> map_ = new TreeMap<String, UIComponent>(String.CASE_INSENSITIVE_ORDER); private Object object_; public ComponentPropertyMap(final Object object) { object_ = object; } @Override public UIComponent getValue(final Object key) { if(!(key instanceof String)) { throw new IllegalArgumentException("key must be a String"); } return map_.get(key); } @Override public void setValue(final Object key, final Object value) { if(!(key instanceof String)) { throw new IllegalArgumentException("key must be a String"); } if(!(value instanceof UIComponent)) { throw new IllegalArgumentException("value must be a UIComponent"); } map_.put((String)key, (UIComponent)value); } @Override public boolean isReadOnly(final Object key) { return false; } @Override public Class<?> getType(final Object key) { return UIComponent.class; } public void clear() { map_.clear(); } @SuppressWarnings("unchecked") public void initialize() { if(object_ == null) { return; } FacesContext facesContext = FacesContext.getCurrentInstance(); ApplicationEx app = (ApplicationEx)facesContext.getApplication(); // Now search for an appropriate adapter List<ComponentMapAdapterFactory> factories = app.findServices(COMPONENT_MAP_SERVICE_NAME); ComponentMapAdapter adapter = null; for(ComponentMapAdapterFactory fac : factories) { adapter = fac.createAdapter(object_); if(adapter != null) { break; } } // If we didn't find any, there's no work to do if(adapter == null) { return; } ResourceBundle translation = adapter.getTranslationBundle(); for(Map.Entry<String, UIComponent> entry : map_.entrySet()) { UIComponent component = entry.getValue(); String property = entry.getKey(); String clientId = component.getClientId(facesContext); if(!initialized_.contains(clientId)) { ValueBinding binding = component.getValueBinding("binding"); if(component instanceof UIInput || component instanceof UIOutput) { attachValueBinding(component, binding); if(component instanceof UIInput) { attachConverterAndValidators(component, adapter, property, translation); } } else if(component instanceof UIFormLayoutRow) { populateFormRow((UIFormLayoutRow)component, adapter, property, binding, translation); } else if(component instanceof XspColumn) { populateColumn((XspColumn)component, adapter, property, binding, translation); } else if(component instanceof UIFormTable) { for(String propertyName : adapter.getPropertyNames()) { UIFormLayoutRow formRow = new UIFormLayoutRow(); component.getChildren().add(formRow); formRow.setParent(component); // Create a binding for the field by replacing the ".all" at the end with the field name String bindingString = FrameworkUtils.strLeftBack(binding.getExpressionString(), ".all") + "." + propertyName + "}"; ValueBinding rowBinding = ApplicationEx.getInstance().createValueBinding(bindingString); populateFormRow(formRow, adapter, propertyName, rowBinding, translation); } } else if(component instanceof XspViewColumn) { // TODO make this work XspViewColumn column = (XspViewColumn)component; column.setColumnName(property); if(column.getHeader() == null) { XspViewColumnHeader header = new XspViewColumnHeader(); header.setValue(adapter.getTranslationForProperty(property)); column.getChildren().add(header); header.setParent(column); column.setHeader(header); } } initialized_.add(clientId); } } } private void populateFormRow(final UIFormLayoutRow formRow, final ComponentMapAdapter adapter, final String property, final ValueBinding binding, final ResourceBundle translation) { if(StringUtil.isEmpty(formRow.getLabel())) { formRow.setLabel(adapter.getTranslationForProperty(property)); } UIComponent input = findOrCreateInput(formRow, adapter, property); attachValueBinding(input, binding); attachConverterAndValidators(input, adapter, property, translation); } @SuppressWarnings("unchecked") private void populateColumn(final XspColumn column, final ComponentMapAdapter adapter, final String property, final ValueBinding binding, final ResourceBundle translation) { Map<String, UIComponent> facets = column.getFacets(); if(!facets.containsKey("header")) { String label = adapter.getTranslationForProperty(property); if(StringUtil.isNotEmpty(label)) { XspOutputText text = new XspOutputText(); text.setValue(label); facets.put("header", text); } } UIComponent input = findOrCreateInput(column, adapter, property); attachValueBinding(input, binding); attachConverterAndValidators(input, adapter, property, translation); } @SuppressWarnings("unchecked") private UIComponent findOrCreateInput(final UIComponent component, final ComponentMapAdapter adapter, final String property) { UIComponent input = null; if(component.getChildCount() == 0) { input = createComponent(adapter, property); component.getChildren().add(input); input.setParent(component); } else { // Otherwise, check for an input control with no value binding List<UIComponent> inputControls = new XspQuery().addInstanceOf(UIInput.class).locate(component); for(UIComponent control : inputControls) { if(control.getValueBinding("value") == null) { input = control; break; } } if(input == null) { input = createComponent(adapter, property); component.getChildren().add(0, input); input.setParent(component); } } return input; } @SuppressWarnings("unchecked") private UIComponent createComponent(final ComponentMapAdapter adapter, final String property) { UIInput input; Type valueType = adapter.getGenericType(property); if(valueType instanceof Class && ((Class<?>)valueType).isEnum()) { // Single-value enum input = new XspSelectOneMenu(); } else if(valueType instanceof ParameterizedType) { ParameterizedType ptype = (ParameterizedType)valueType; if(Collection.class.isAssignableFrom((Class<?>)ptype.getRawType())) { // Then it's a collection of one form or another with one and only one type argument Type genericType = ptype.getActualTypeArguments()[0]; if(String.class.equals(genericType)) { input = new XspInputText(); ((XspInputText)input).setMultipleSeparator(";"); ((XspInputText)input).setMultipleTrim(true); } else if(genericType instanceof Class && ((Class<?>)genericType).isEnum()) { input = new XspSelectManyListbox(); input.getAttributes().put("multiple", Boolean.TRUE); } else { input = new XspInputText(); } } else { // Punt back to single-value text input = new XspInputText(); } } else if(Boolean.class.equals(valueType) || Boolean.TYPE.equals(valueType)) { input = new XspSelectOneRadio(); } else { // Assume single-value text input = new XspInputText(); } return input; } private void attachValueBinding(final UIComponent component, final ValueBinding binding) { if(component.getValueBinding("value") == null) { Pattern bindingPattern = Pattern.compile("^\\#\\{" + ControllingViewHandler.BEAN_NAME + "\\." + controllerPropertyName_ + "\\[(.*)\\]((\\.|\\[).*)\\}$"); Matcher matcher = bindingPattern.matcher(binding.getExpressionString()); if(matcher.matches()) { String modelName = matcher.group(1); String elProp = matcher.group(2); String valueString = "#{" + modelName + elProp + "}"; component.setValueBinding("value", FacesContext.getCurrentInstance().getApplication().createValueBinding(valueString)); } } } @SuppressWarnings({ "rawtypes", "unchecked" }) private void attachConverterAndValidators(final UIComponent component, final ComponentMapAdapter adapter, final Object property, final ResourceBundle translation) { UIInput input = (UIInput)component; /* ****************************************************************************** * Add support based on constraints ********************************************************************************/ Set<ConstraintDescriptor<?>> constraints = adapter.getConstraintDescriptors(property); if(!constraints.isEmpty()) { boolean required = false; for(ConstraintDescriptor<?> desc : constraints) { // First, add basic required support Object annotation = desc.getAnnotation(); if(annotation instanceof NotNull || annotation instanceof NotEmpty) { required = true; break; } else if(annotation instanceof Size && ((Size)annotation).min() > 0) { required = true; } } if(required) { input.setRequired(true); } // Now, add arbitrary validators Validator validator = adapter.createValidator(property); if(validator != null) { input.addValidator(validator); } } /* ****************************************************************************** * Add support based on the property type ********************************************************************************/ Type valueType = adapter.getGenericType(property); // Determine if we're dealing with a Collection or not Type baseType; if(valueType instanceof ParameterizedType) { ParameterizedType ptype = (ParameterizedType)valueType; if(Collection.class.isAssignableFrom((Class<?>)ptype.getRawType())) { baseType = ptype.getActualTypeArguments()[0]; } else { baseType = valueType; } } else { baseType = valueType; } // Add selectItems for single-value enums if(baseType instanceof Class && ((Class<?>)baseType).isEnum()) { Class<? extends Enum> enumType = (Class<? extends Enum>)baseType; if(input.getConverter() == null) { input.setConverter(new EnumBindingConverter(enumType)); } // Look for select items in its children List<UIComponent> children = input.getChildren(); boolean hasSelectItems = false; for(UIComponent child : children) { if(child instanceof UISelectItem || child instanceof UISelectItems) { hasSelectItems = true; break; } } if(!hasSelectItems) { if(input instanceof UISelectOne || input instanceof UISelectMany) { if(input instanceof UISelectOneMenu) { addEmptySelectItem(component, enumType, translation); } populateEnumSelectItems(input, enumType, translation); } } } else if(Boolean.class.equals(baseType) || Boolean.TYPE.equals(baseType)) { // Do the same for booleans UISelectItem itemTrue = new UISelectItem(); itemTrue.setItemValue(Boolean.TRUE); UISelectItem itemFalse = new UISelectItem(); itemFalse.setItemValue(Boolean.FALSE); if(translation != null) { String trueKey = adapter.getObject().getClass().getName() + "." + property + ".true"; try { itemTrue.setItemLabel(translation.getString(trueKey)); } catch(Exception e) { try { // Then see if there's a generic catchall itemTrue.setItemLabel(translation.getString("true")); } catch(Exception e2) { // If all else fails... itemTrue.setItemLabel("true"); } } String falseKey = adapter.getObject().getClass().getName() + "." + property + ".false"; try { itemFalse.setItemLabel(translation.getString(falseKey)); } catch(Exception e) { try { // Then see if there's a generic catchall itemFalse.setItemLabel(translation.getString("false")); } catch(Exception e2) { // If all else fails... itemFalse.setItemLabel("false"); } } } component.getChildren().add(itemTrue); itemTrue.setParent(component); component.getChildren().add(itemFalse); itemFalse.setParent(component); } // Add a converter and helper for date/time fields if(baseType.equals(Date.class)) { if(input.getConverter() == null) { DateTimeConverter converter = new DateTimeConverter(); converter.setType(DateTimeConverter.TYPE_BOTH); input.setConverter(converter); } XspDateTimeHelper helper = new XspDateTimeHelper(); component.getChildren().add(helper); helper.setParent(component); } else if(baseType.equals(java.sql.Date.class)) { if(input.getConverter() == null) { DateTimeConverter converter = new DateTimeConverter(); converter.setType(DateTimeConverter.TYPE_DATE); input.setConverter(converter); } XspDateTimeHelper helper = new XspDateTimeHelper(); component.getChildren().add(helper); helper.setParent(component); } else if(baseType.equals(java.sql.Time.class)) { if(input.getConverter() == null) { DateTimeConverter converter = new DateTimeConverter(); converter.setType(DateTimeConverter.TYPE_TIME); input.setConverter(converter); } XspDateTimeHelper helper = new XspDateTimeHelper(); component.getChildren().add(helper); helper.setParent(component); } } @SuppressWarnings("unchecked") private void addEmptySelectItem(final UIComponent component, final Class<?> enumType, final ResourceBundle translation) { UISelectItem empty = new UISelectItem(); try { String transKey = enumType.getName() + ".(SELECT_ONE)"; empty.setItemLabel(translation.getString(transKey)); } catch(Exception e) { try { empty.setItemLabel(translation.getString("(SELECT_ONE)")); } catch(Exception e2) { empty.setItemLabel(" - Select One -"); } } empty.setItemValue(""); component.getChildren().add(empty); empty.setParent(component); } @SuppressWarnings("unchecked") private void populateEnumSelectItems(final UIComponent component, final Class<?> enumType, final ResourceBundle translation) { Enum<?>[] constants = (Enum<?>[])enumType.getEnumConstants(); for(Enum<?> constant : constants) { UISelectItem item = new UISelectItem(); // Look for a localized label String label = constant.name(); if(translation != null) { String transKey = enumType.getName() + "." + constant.name(); try { label = translation.getString(transKey); } catch(Exception e) { // Ignore } } item.setItemLabel(label); item.setItemValue(constant); component.getChildren().add(item); item.setParent(component); } } } }